#include "vtkFieldImport_SWEBoundary.hpp"

#include "bc_swe_all.hpp"
#include "vtkFunctions_import.hpp"
#include "vtkFunctions.hpp"
#include "magic_enum.hpp"

namespace hms::vtk
{


template<typename MeshType>
std::optional<boundaryCondition::Type> stringToBoundaryType(
	SolverBase<SWESolver<MeshType>>&,
	typename SolverBase<SWESolver<MeshType>>::FieldName fieldName,
	std::string_view bcTypeStr
){
	using F = typename SWESolver<MeshType>::FieldName;
	using T = boundaryCondition::Type;
	using magic_enum::enum_cast;
	switch(fieldName){
		case F::Velocity:
			return enum_cast<T>( std::string{bcTypeStr} + "_velocity" );
		case F::WaterDepth:
			if ( bcTypeStr == "slip" )
				return T::zeroGradient;
			else if ( bcTypeStr == "constantWaterDepth" )
				return T::staticValue;
			else
				return enum_cast<T>( std::string{bcTypeStr} + "_waterdepth" );
			break;
		case F::BottomElevation:
			return enum_cast<T>( std::string{bcTypeStr} + "_bottomElevation" );
		default:
			;
	}
	return std::nullopt;
}


template<typename MeshType>
void readSWEBoundaryCondition(
	const xml::XMLElement* bcElem,
	const BoundaryPatch& patch,
	SolverBase<SWESolver<MeshType>>& solver,
	typename SolverBase<SWESolver<MeshType>>::FieldName fieldName,
	boundaryCondition::Type bcType
){
	using T = boundaryCondition::Type;
	using F = typename SolverBase<SWESolver<MeshType>>::FieldName;
	using magic_enum::enum_name;

	auto checkMatchingFields = [&](F correctField){
		if ( fieldName != correctField )
			throw ParseError(
				"Boundary condition for patch \""
				+ patch.name()
				+ "\" set to \""s
				+ std::string{ enum_name(bcType) }
				+ "\", but field is \"" 
				+ std::string{ enum_name(fieldName) }
				+ "\". Boundary condition only valid for field \""
				+ std::string{ enum_name(correctField) }
				+ "\"!\n"
				+ nodeString(bcElem)
			);
	};

	auto readOrthVelocity = [&](){
		return getAttributeValue<scalar>(bcElem, "OrthogonalVelocity");
	};
	auto readWeirSlope = [&]() -> scalar {
		bool isRadians { getAttributeValueWithDefault<bool>(
			bcElem, "isRadians", false
		) };
		auto slope { getAttributeValue<scalar>(bcElem, "angle") };
		auto enforceLimits = [&]( scalar lb, scalar ub, std::string_view unit ){
			if ( slope < lb || slope > ub )
				throw InputError( fmt::format(
					"Slope angle is {0}, "
					"but must be between {1} and {2} [{3}]!\n{4}",
					bold(red("{: .3g}", slope)),
					lb, ub, unit,
					nodeString(bcElem)
				) );
		};
		if ( isRadians ){
			enforceLimits(0, math::pi/2, "rad");
		} else {
			enforceLimits(0, 90, "deg");
			slope = degToRad(slope);
		}
		return slope;
	};

	Field<MeshType>& field { solver.field(fieldName) };

	switch(bcType){
		case T::constantWaterDepth_velocity: {
			checkMatchingFields(F::Velocity);
			auto dGhost { getAttributeValue<scalar>(bcElem, "WaterDepth") };
			field.template
				setBoundaryCondition<ConstantWaterDepth_velocity<MeshType>>(
					patch, solver.field(F::WaterDepth), dGhost
				);
			break;
		}
		case T::freeOutflow_velocity: {
			checkMatchingFields(F::Velocity);
			field.template
				setBoundaryCondition<FreeOutflow_velocity<MeshType>>(patch);
			break;
		}
		case T::freeOutflow_waterdepth: {
			checkMatchingFields(F::WaterDepth);
			auto isLimited { getAttributeValue<bool>(bcElem, "isLimited") };
			scalar limit {0};
			if ( isLimited )
				limit = getAttributeValue<scalar>(bcElem, "limit");
			field.template
				setBoundaryCondition<FreeOutflow_waterdepth<MeshType>>(
					patch, isLimited, limit
				);
			break;
		}
		case T::orthogonalDischarge_velocity: {
			checkMatchingFields(F::Velocity);
			auto discharge { getAttributeValue<scalar>(bcElem, "discharge") };
			field.setBoundaryCondition( OrthogonalDischarge_velocity<MeshType>{
				field, patch, solver.field(F::WaterDepth), discharge
			} );
			break;
		}
		case T::orthogonalDischarge_waterdepth: {
			checkMatchingFields(F::WaterDepth);
			field.setBoundaryCondition(
				OrthogonalDischarge_waterdepth<MeshType>{ field, patch }
			);
			break;
		}
		case T::orthogonalFlowVelocity_velocity: {
			checkMatchingFields(F::Velocity);
			field.template
				setBoundaryCondition<OrthogonalFlowVelocity_velocity<MeshType>>(
					patch, readOrthVelocity()
				);
			break;
		}
		case T::orthogonalFlowVelocity_waterdepth: {
			checkMatchingFields(F::WaterDepth);
			using OFV_w = OrthogonalFlowVelocity_waterdepth<MeshType>;
			field.template setBoundaryCondition<OFV_w>(
				patch, solver.field(F::Velocity), readOrthVelocity()
			);
			break;
		}
		case T::slip_velocity: {
			checkMatchingFields(F::Velocity);
			field.template setBoundaryCondition<Slip_velocity<MeshType>>(patch);
			break;
		}
		case T::weir_bottomElevation: {
			checkMatchingFields(F::BottomElevation);
			field.setBoundaryCondition( Weir_bottomElevation{ field, patch,
				solver.field(F::WaterDepth),
				solver.field(F::Velocity),
				readWeirSlope()
			} );
			break;
		}
		case T::weir_velocity: {
			checkMatchingFields(F::Velocity);
			field.setBoundaryCondition( Weir_velocity{ field, patch,
				solver.field(F::WaterDepth),
				readWeirSlope()
			} );
			break;
		}
		case T::weir_waterdepth: {
			checkMatchingFields(F::WaterDepth);
			field.setBoundaryCondition( Weir_waterdepth{ field, patch,
				solver.field(F::Velocity),
				readWeirSlope()
			} );
			break;
		}
		default:
			throw ParseError( fmt::format(
				"Boundary condition type {}: "
				"import not implemented or not SWE boundary type!",
				bold( enum_name(bcType) )
			) );
	}
}


#define INSTANTIATE_FOR_ALL_MESHTYPES(MeshType) \
template void readSWEBoundaryCondition( const xml::XMLElement*, \
	const BoundaryPatch&, SolverBase<SWESolver<MeshType>>&, \
	typename SolverBase<SWESolver<MeshType>>::FieldName, \
	boundaryCondition::Type ); \
template std::optional<boundaryCondition::Type> stringToBoundaryType( \
	SolverBase<SWESolver<MeshType>>&, \
	typename SolverBase<SWESolver<MeshType>>::FieldName, \
	std::string_view );

#include "meshInstantiator.hpp"

} // namespace hms::vtk
